View Javadoc
1   package org.apache.maven.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  
27  import org.apache.maven.shared.utils.StringUtils;
28  
29  /**
30   * @author Kristian Rosenvold
31   */
32  @SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
33  public class SmartStackTraceParser
34  {
35      private static final int MAX_LINE_LENGTH = 77;
36  
37      private final SafeThrowable throwable;
38  
39      private final StackTraceElement[] stackTrace;
40  
41      private final String simpleName;
42  
43      private String testClassName;
44  
45      private final Class testClass;
46  
47      private String testMethodName;
48  
49      public SmartStackTraceParser( Class testClass, Throwable throwable )
50      {
51          this( testClass.getName(), throwable, null );
52      }
53  
54      public SmartStackTraceParser( String testClassName, Throwable throwable, String testMethodName )
55      {
56          this.testMethodName = testMethodName;
57          this.testClassName = testClassName;
58          testClass = getClass( testClassName );
59          simpleName = testClassName.substring( testClassName.lastIndexOf( "." ) + 1 );
60          this.throwable = new SafeThrowable( throwable );
61          stackTrace = throwable.getStackTrace();
62      }
63  
64      private static Class getClass( String name )
65      {
66          try
67          {
68              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
69              return classLoader != null ? classLoader.loadClass( name ) : null;
70          }
71          catch ( ClassNotFoundException e )
72          {
73              return null;
74          }
75      }
76  
77      private static String getSimpleName( String className )
78      {
79          int i = className.lastIndexOf( "." );
80          return className.substring( i + 1 );
81      }
82  
83      @SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
84      public String getString()
85      {
86          if ( testClass == null )
87          {
88              return throwable.getLocalizedMessage();
89          }
90  
91          final StringBuilder result = new StringBuilder();
92          final List<StackTraceElement> stackTraceElements = focusOnClass( stackTrace, testClass );
93          Collections.reverse( stackTraceElements );
94          if ( stackTraceElements.isEmpty() )
95          {
96              result.append( simpleName );
97              if ( StringUtils.isNotEmpty( testMethodName ) )
98              {
99                  result.append( "." )
100                     .append( testMethodName );
101             }
102         }
103         else
104         {
105             for ( int i = 0; i < stackTraceElements.size(); i++ )
106             {
107                 final StackTraceElement stackTraceElement = stackTraceElements.get( i );
108                 if ( i == 0 )
109                 {
110                     result.append( simpleName );
111                     if ( !stackTraceElement.getClassName().equals( testClassName ) )
112                     {
113                         result.append( ">" );
114                     }
115                     else
116                     {
117                         result.append( "." );
118                     }
119                 }
120                 if ( !stackTraceElement.getClassName().equals( testClassName ) )
121                 {
122                     result.append( getSimpleName( stackTraceElement.getClassName() ) ) // Add the name of the superclas
123                         .append( "." );
124                 }
125                 result.append( stackTraceElement.getMethodName() )
126                     .append( ":" )
127                     .append( stackTraceElement.getLineNumber() )
128                     .append( "->" );
129             }
130 
131             if ( result.length() >= 2 )
132             {
133                 result.deleteCharAt( result.length() - 1 )
134                     .deleteCharAt( result.length() - 1 );
135             }
136         }
137 
138         Throwable target = throwable.getTarget();
139         if ( target instanceof AssertionError )
140         {
141             result.append( " " )
142                 .append( throwable.getMessage() );
143         }
144         else if ( "junit.framework.AssertionFailedError".equals( target.getClass().getName() )
145             || "junit.framework.ComparisonFailure".equals( target.getClass().getName() ) )
146         {
147             result.append( " " );
148             result.append( throwable.getMessage() );
149         }
150         else
151         {
152             result.append( rootIsInclass() ? " " : " ยป " );
153             result.append( getMinimalThrowableMiniMessage( target ) );
154             result.append( getTruncatedMessage( MAX_LINE_LENGTH - result.length() ) );
155         }
156         return result.toString();
157     }
158 
159     private String getMinimalThrowableMiniMessage( Throwable throwable )
160     {
161         String name = throwable.getClass().getSimpleName();
162         if ( name.endsWith( "Exception" ) )
163         {
164             return StringUtils.chompLast( name, "Exception" );
165         }
166         if ( name.endsWith( "Error" ) )
167         {
168             return StringUtils.chompLast( name, "Error" );
169         }
170         return name;
171     }
172 
173     private String getTruncatedMessage( int i )
174     {
175         if ( i < 0 )
176         {
177             return "";
178         }
179         String msg = throwable.getMessage();
180         if ( msg == null )
181         {
182             return "";
183         }
184         String substring = msg.substring( 0, Math.min( i, msg.length() ) );
185         if ( i < msg.length() )
186         {
187             return " " + substring + "...";
188         }
189         else
190         {
191             return " " + substring;
192         }
193     }
194 
195     private boolean rootIsInclass()
196     {
197         return stackTrace.length > 0 && stackTrace[0].getClassName().equals( testClassName );
198     }
199 
200     static List<StackTraceElement> focusOnClass( StackTraceElement[] stackTrace, Class clazz )
201     {
202         List<StackTraceElement> result = new ArrayList<StackTraceElement>();
203         for ( StackTraceElement element : stackTrace )
204         {
205             if ( element != null && isInSupers( clazz, element.getClassName() ) )
206             {
207                 result.add( element );
208             }
209         }
210         return result;
211     }
212 
213     private static boolean isInSupers( Class testClass, String lookFor )
214     {
215         if ( lookFor.startsWith( "junit.framework." ) )
216         {
217             return false;
218         }
219         while ( !testClass.getName().equals( lookFor ) && testClass.getSuperclass() != null )
220         {
221             testClass = testClass.getSuperclass();
222         }
223         return testClass.getName().equals( lookFor );
224     }
225 
226     static Throwable findTopmostWithClass( final Throwable t, StackTraceFilter filter )
227     {
228         Throwable n = t;
229         do
230         {
231             if ( containsClassName( n.getStackTrace(), filter ) )
232             {
233                 return n;
234             }
235 
236             n = n.getCause();
237 
238         }
239         while ( n != null );
240         return t;
241     }
242 
243     public static String stackTraceWithFocusOnClassAsString( Throwable t, String className )
244     {
245         StackTraceFilter filter = new ClassNameStackTraceFilter( className );
246         Throwable topmost = findTopmostWithClass( t, filter );
247         List<StackTraceElement> stackTraceElements = focusInsideClass( topmost.getStackTrace(), filter );
248         String s = causeToString( topmost.getCause(), filter );
249         return toString( t, stackTraceElements, filter ) + s;
250     }
251 
252     static List<StackTraceElement> focusInsideClass( StackTraceElement[] stackTrace, StackTraceFilter filter )
253     {
254         List<StackTraceElement> result = new ArrayList<StackTraceElement>();
255         for ( StackTraceElement element : stackTrace )
256         {
257             if ( filter.matches( element ) )
258             {
259                 result.add( element );
260             }
261         }
262         return result;
263     }
264 
265     static boolean containsClassName( StackTraceElement[] stackTrace, StackTraceFilter filter )
266     {
267         for ( StackTraceElement element : stackTrace )
268         {
269             if ( filter.matches( element ) )
270             {
271                 return true;
272             }
273         }
274         return false;
275     }
276 
277     private static String causeToString( Throwable cause, StackTraceFilter filter )
278     {
279         String resp = "";
280         while ( cause != null )
281         {
282             resp += "Caused by: ";
283             resp += toString( cause, Arrays.asList( cause.getStackTrace() ), filter );
284             cause = cause.getCause();
285         }
286         return resp;
287     }
288 
289     private static String toString( Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter )
290     {
291         String result = "";
292         if ( t != null )
293         {
294             result += t.getClass().getName();
295             String msg = t.getMessage();
296             if ( msg != null )
297             {
298                 result += ": ";
299                 if ( isMultiLine( msg ) )
300                 {
301                     // SUREFIRE-986
302                     result += "\n";
303                 }
304                 result += msg;
305             }
306             result += "\n";
307         }
308 
309         for ( StackTraceElement element : elements )
310         {
311             if ( filter.matches( element ) )
312             {
313                 result += "\tat ";
314                 result += element;
315                 result += "\n";
316             }
317         }
318         return result;
319     }
320 
321     private static boolean isMultiLine( String msg )
322     {
323         int countNewLines = 0;
324         for ( int i = 0, length = msg.length(); i < length; i++ )
325         {
326             if ( msg.charAt( i ) == '\n' )
327             {
328                 if ( ++countNewLines == 2 )
329                 {
330                     break;
331                 }
332             }
333         }
334         return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith( "\n" );
335     }
336 }